/* Engine of LOCKJAW, an implementation of the Soviet Mind Game

Copyright (C) 2006 Damian Yerrick <tepples+lj@spamcop.net>

This work is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

Original game concept and design by Alexey Pajitnov.
The Software is not sponsored or endorsed by Alexey Pajitnov, Elorg,
or The Tetris Company LLC.


*/

package fizzpicks.tetrapulp.tetrapulp;

import net.jscience.math.MathFP;

import java.util.EnumSet;
import java.util.Set;
import static java.lang.System.*;

/**
 * Created by will
 * Date: May 2, 2008 11:34:10 PM
 */
public class Lockjaw extends LJPulp {

    /**
     * Random number generator.  Use this for all random events
     * that affect the game state.
     *
     * @param p field
     * @return uniformly distributed number in 0 to 0x7FFF
     */
    static long ljRand(LJField p) {
        p.seed = p.seed * 2147001325 + 715136305;
        return Math.abs(p.seed >> 17);
    }

    static int ljRandInt(LJField p) {
        p.seed = p.seed * 2147001325 + 715136305;
        return Math.abs((int)(p.seed >> 17));
    }

    static void ljAssert(LJField p, boolean shouldBeTrue, final String reason) {
        if (!shouldBeTrue) {
            p.state = LJState.GAMEOVER;
        }
    }

/*
    enum connect {
        CONNECT_U,
        CONNECT_R,
        CONNECT_D,
        CONNECT_L;
        connect get(int x) {
            switch(x) {
                default:
                case 0: return CONNECT_U;
                case 1: return CONNECT_R;
                case 2: return CONNECT_D;
                case 3: return CONNECT_L;
            }
        }
    }
    static final Set<connect> CONNECT_NONE = EnumSet.noneOf(connect.class);
    static final Set<connect> CONNECT_U = EnumSet.of(connect.CONNECT_U);
    static final Set<connect> CONNECT_R = EnumSet.of(connect.CONNECT_R);
    static final Set<connect> CONNECT_D = EnumSet.of(connect.CONNECT_D);
    static final Set<connect> CONNECT_L = EnumSet.of(connect.CONNECT_L);
    static final Set<connect> CONNECT_UD = EnumSet.of(connect.CONNECT_U, connect.CONNECT_D);
    static final Set<connect> CONNECT_UL = EnumSet.of(connect.CONNECT_U, connect.CONNECT_L);
    static final Set<connect> CONNECT_DL = EnumSet.of(connect.CONNECT_D, connect.CONNECT_L);
    static final Set<connect> CONNECT_UR = EnumSet.of(connect.CONNECT_U, connect.CONNECT_R);
    static final Set<connect> CONNECT_DR = EnumSet.of(connect.CONNECT_D, connect.CONNECT_R);
    static final Set<connect> CONNECT_LR = EnumSet.of(connect.CONNECT_L, connect.CONNECT_R);
    static final Set<connect> CONNECT_UDL = EnumSet.of(connect.CONNECT_U, connect.CONNECT_D, connect.CONNECT_L);
    static final Set<connect> CONNECT_UDR = EnumSet.of(connect.CONNECT_U, connect.CONNECT_D, connect.CONNECT_R);
    static final Set<connect> CONNECT_ULR = EnumSet.of(connect.CONNECT_U, connect.CONNECT_L, connect.CONNECT_R);
    static final Set<connect> CONNECT_DLR = EnumSet.of(connect.CONNECT_D, connect.CONNECT_L, connect.CONNECT_R);
    static final Set<connect> CONNECT_UDLR = EnumSet.of(connect.CONNECT_U, connect.CONNECT_D, connect.CONNECT_L, connect.CONNECT_R);
    static final Set<connect> CONNECT_MASK = CONNECT_UDLR;
*/

    public static final int
            CONNECT_NONE = -1,
            CONNECT_U = 0x01,
            CONNECT_R = 0x02,
            CONNECT_D = 0x04,
            CONNECT_L = 0x08,
            CONNECT_UD = CONNECT_U | CONNECT_D,
            CONNECT_UL = CONNECT_U | CONNECT_L,
            CONNECT_DL = CONNECT_D | CONNECT_L,
            CONNECT_UR = CONNECT_U | CONNECT_R,
            CONNECT_DR = CONNECT_D | CONNECT_R,
            CONNECT_LR = CONNECT_L | CONNECT_R,
            CONNECT_UDL = CONNECT_UD | CONNECT_L,
            CONNECT_UDR = CONNECT_UD | CONNECT_R,
            CONNECT_ULR = CONNECT_UL | CONNECT_R,
            CONNECT_DLR = CONNECT_DL | CONNECT_R,
            CONNECT_UDLR = CONNECT_UD | CONNECT_LR,
            CONNECT_MASK = CONNECT_UDLR;

    public static final int COLOR_MASK = 0xF0;

    // Guideline says 10 wide, but we want to support tetrinet mode
    public static final int LJ_PF_WID = 12;
    public static final int LJ_SPAWN_X = ((LJ_PF_WID - 3) / 2);
    public static final int LJ_PF_HT = 24;
    public static final int LJ_PF_VIS_HT = 20;
    public static final int LJ_NEXT_PIECES = 8;
    public static final int LJ_MAX_LINES_PER_PIECE = 8;

    public static final int MAX_BAG_LEN = 10;
    public static final int MAX_OMINO = 4;

    public static final int KICK_TABLE_LEN = 5;
    public static int WallKickTable[][] = new int[4][KICK_TABLE_LEN];



    static final int[][][] xShapes = {
            {{0, 1, 2, 3}, {2, 2, 2, 2}, {3, 2, 1, 0}, {1, 1, 1, 1}}, // I
            {{0, 1, 2, 0}, {1, 1, 1, 2}, {2, 1, 0, 2}, {1, 1, 1, 0}}, // J
            {{0, 1, 2, 2}, {1, 1, 1, 2}, {2, 1, 0, 0}, {1, 1, 1, 0}}, // L
            {{1, 1, 2, 2}, {1, 2, 2, 1}, {2, 2, 1, 1}, {2, 1, 1, 2}}, // O
            {{0, 1, 1, 2}, {1, 1, 2, 2}, {2, 1, 1, 0}, {1, 1, 0, 0}}, // S
            {{0, 1, 2, 1}, {1, 1, 1, 2}, {2, 1, 0, 1}, {1, 1, 1, 0}}, // T
            {{0, 1, 1, 2}, {2, 2, 1, 1}, {2, 1, 1, 0}, {0, 0, 1, 1}}, // Z
            {{0, 1, 2, 3}, {2, 2, 2, 2}, {3, 2, 1, 0}, {1, 1, 1, 1}}, // I2
            {{0, 1, 2, 0}, {1, 1, 1, 2}, {2, 1, 0, 2}, {1, 1, 1, 0}}, // I3
            {{1, 1, 2, 2}, {1, 2, 2, 1}, {2, 2, 1, 1}, {2, 1, 1, 2}}, // L3
    };

    static final int[][][] yShapes = {
            {{2, 2, 2, 2}, {3, 2, 1, 0}, {1, 1, 1, 1}, {0, 1, 2, 3}}, // I
            {{2, 2, 2, 3}, {3, 2, 1, 3}, {2, 2, 2, 1}, {1, 2, 3, 1}}, // J
            {{2, 2, 2, 3}, {3, 2, 1, 1}, {2, 2, 2, 1}, {1, 2, 3, 3}}, // L
            {{2, 3, 3, 2}, {3, 3, 2, 2}, {3, 2, 2, 3}, {2, 2, 3, 3}}, // O
            {{2, 2, 3, 3}, {3, 2, 2, 1}, {2, 2, 1, 1}, {1, 2, 2, 3}}, // S
            {{2, 2, 2, 3}, {3, 2, 1, 2}, {2, 2, 2, 1}, {1, 2, 3, 2}}, // T
            {{3, 3, 2, 2}, {3, 2, 2, 1}, {1, 1, 2, 2}, {1, 2, 2, 3}}, // Z
            {{2, 2, 2, 2}, {3, 2, 1, 0}, {1, 1, 1, 1}, {0, 1, 2, 3}}, // I2
            {{2, 2, 2, 3}, {3, 2, 1, 3}, {2, 2, 2, 1}, {1, 2, 3, 1}}, // I3
            {{2, 3, 3, 2}, {3, 3, 2, 2}, {3, 2, 2, 3}, {2, 2, 3, 3}}, // L3
    };

    public static final int pieceColors[] = {
            0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70,
            0x40, 0x10, 0x20
    };

    static final int[][] connShapes = {
            {CONNECT_R, CONNECT_LR, CONNECT_LR, CONNECT_L},  // I
            {CONNECT_UR, CONNECT_LR, CONNECT_L, CONNECT_D},  // J
            {CONNECT_R, CONNECT_LR, CONNECT_UL, CONNECT_D},  // L
            {CONNECT_UR, CONNECT_DR, CONNECT_DL, CONNECT_UL}, // O
            {CONNECT_R, CONNECT_UL, CONNECT_DR, CONNECT_L},  // S
            {CONNECT_R, CONNECT_ULR, CONNECT_L, CONNECT_D},  // T
            {CONNECT_R, CONNECT_DL, CONNECT_UR, CONNECT_L},  // Z
            {CONNECT_NONE, CONNECT_R, CONNECT_L, CONNECT_NONE},         // I2
            {CONNECT_R, CONNECT_LR, CONNECT_L, CONNECT_NONE},         // I3
            {CONNECT_UR, CONNECT_D, CONNECT_NONE, CONNECT_L}  // L3
    };

    static int pieceToFieldBlock(int piece, int conn) {
        return conn | pieceColors[piece];
    }

    /**
     * Expands a tetromino to the blocks that make it up.
     *
     * @param out   an array of length 4
     * @param p     the field into which the tetromino will be spawned
     *              (used for default spawn orientation)
     * @param piece a piece number
     * @param xBase x coordinate of base position of the tetromino
     * @param yBase y coordinate of base position of the tetromino
     * @param theta the orientation of the tetromino
     *              (0-3: use this; 4: use default spawn rotation)
     */
    public static void expandPieceToBlocks(LJBlkSpec out[],
                                    LJField p,
                                    LJP piece, int xBase, int yBase, int theta) {
        int shape = piece.index & LJP.MASK.index;

        if (theta >= 4) {
            LJRotSystem rs = WK.rotSystems.get(p.rotationSystem);
            int kickData = rs.entryOffset[shape];
            xBase += WK.WKX(kickData);
            yBase += WK.WKY(kickData);
            kickData = rs.entryOffset[LJP.I.index];
            xBase -= WK.WKX(kickData);
            yBase -= WK.WKY(kickData);
            theta = rs.entryTheta[shape];
        }

        final int[] xBl = xShapes[shape][theta];
        final int[] yBl = yShapes[shape][theta];

        for (int blk = 0; blk < 4; ++blk) {
            if (connShapes[shape][blk] == -1) {
                out[blk].conn = 0;
            } else {
                int conn = connShapes[shape][blk] << theta;
                int connRotated = (conn | (conn >> 4)) & CONNECT_MASK;
                int color = ((0x10 << blk) & piece.index) != 0 ? 8 : piece.index;
                out[blk].y = yBl[blk] + yBase;
                out[blk].x = xBl[blk] + xBase;
                out[blk].conn = pieceToFieldBlock(color, connRotated);
            }
        }
    }

    /**
     * Tests whether a particular block is occupied.
     *
     * @param p the playfield
     * @param x the column
     * @param y the row (0 = bottom)
     * @return zero iff the space is open
     */
    static boolean isOccupied(final LJField p, int x, int y) {
        if (x < p.leftWall || x >= p.rightWall || y < 0)
            return true;
        if (y > LJ_PF_HT)
            return false;
        return (p.b[y][x] > 1);
    }

    /**
     * Tests whether a particular tetromino would overlap one or more
     * blocks, a side wall, or the floor
     *
     * @param p the playfield
     * @param x the column of the left side of the piece's bounding 4x4
     * @param y the row of the bottom of the piece's bounding 4x4 (0 = bottom)
     * @return false iff the space is open
     */
    static boolean isCollision(final LJField p, int x, int y, int theta) {
        LJBlkSpec blocks[] = new LJBlkSpec[4];
        for (int i = 0; i < blocks.length; i++) { blocks[i] = new LJBlkSpec(); }
        LJP piece = p.curPiece[0];

        expandPieceToBlocks(blocks, p, piece, x, y, theta);

        for (int blk = 0; blk < 4; ++blk) {
            if ((blocks[blk].conn != 0)
                    && isOccupied(p, blocks[blk].x, blocks[blk].y))
                return true;
        }
        return false;
    }

    /**
     * Determines whether the piece that just landed was a T-spin.
     * Must be called just BEFORE lockdown writes the blocks to the
     * playfield; otherwise TNT will break.
     */
    static IsSpin isTspin(final LJField p) {
        int blks = 0;
        int x = p.x;
        int y = p.hardDropY;

        switch (p.tSpinAlgo) {
            case TNT:
                if (!isCollision(p, x, y + 1, p.theta)
                        || !isCollision(p, x - 1, y, p.theta)
                        || !isCollision(p, x + 1, y, p.theta)) {
                    return IsSpin.NONE;
                }
                return p.isSpin;

            case TDS_NO_KICK:

                // If t-spin involved wall kick, don't count it
                if (p.isSpin == IsSpin.TWISTKICK) {
                    return IsSpin.NONE;
                }

                // otherwise fall through
            case TDS:
                // 1. T tetromino
                if ((p.curPiece[0]) != LJP.T) {
                    return IsSpin.NONE;
                }

                // 2. Last move was spin
                if (p.isSpin == IsSpin.NONE) {
                    return IsSpin.NONE;
                }

                // 3. At least three cells around the rotation center are full
                if (isOccupied(p, x, y + 1)) {
                    ++blks;
                }
                if (isOccupied(p, x, y + 3)) {
                    ++blks;
                }
                if (isOccupied(p, x + 2, y + 1)) {
                    ++blks;
                }
                if (isOccupied(p, x + 2, y + 3)) {
                    ++blks;
                }
                if (blks < 3) {
                    return IsSpin.NONE;
                }

                // 3. Last move was spin
                return p.isSpin;
            default:
                return IsSpin.NONE;
        }
    }

    /**
     * Calculates where the active piece in a playfield will fall
     * if dropped, and writes it back to the playfield.
     * This value is used for ghost piece, gravity, soft drop, and
     * hard drop. Call this after the active piece has been spawned,
     * moved, or rotated.
     *
     * @param p the playfield
     */
    static void updHardDropY(LJField p) {
        int x = p.x;
        int y = (int) MathFP.toLong(p.y);
        int theta = p.theta;

        if (p.bottomBlocks != 0) {
            y = -2;
            while (y < (int) LJ_PF_HT
                    && y < MathFP.toLong(p.y)
                    && isCollision(p, x, y, theta)) {
                ++y;
            }
        } else {
            while (!isCollision(p, x, y - 1, theta)) {
                --y;
            }
        }
        p.hardDropY = y;
    }


    /**
     * Look for a TNT square in this position.
     *
     * @param x       column of left side, such that 0 <= x <= playfield width - 4
     * @param y       row of bottom block, such that 0 <= y <= playfield height - 4
     * @param isMulti nonzero for multisquares; 0 for monosquares
     * @return nonzero if found; 0 if not found
     */
    static boolean isSquareAt(LJField p, int x, int y, boolean isMulti) {
        int firstColor = p.b[y][x] & COLOR_MASK;

        // Check the frame to make sure it isn't connected to anything else
        for (int i = 0; i <= 3; i++) {
            /* don't allow squares within parts of squares */
            if ((p.b[y + i][x] & COLOR_MASK) >= 0x80)
                return false;
            /* the block doesn't connect on the left */
            if ((p.b[y + i][x] & CONNECT_L) != 0)
                return false;
            /* the block doesn't connect on the right */
            if ((p.b[y + i][x + 3] & CONNECT_R) != 0)
                return false;
            /* the block doesn't connect on the bottom */
            if ((p.b[y][x + i] & CONNECT_D) != 0)
                return false;
            /* the block doesn't connect on the top */
            if ((p.b[y + 3][x + i] & CONNECT_U) != 0)
                return false;
        }

        for (int ySub = 0; ySub < 4; ++ySub) {
            for (int xSub = 0; xSub <= 3; ++xSub) {
                int blkHere = p.b[y + ySub][x + xSub];

                /* the square contains no nonexistent blocks */
                if (blkHere == 0)
                    return false;
                /* the square contains no blocks of garbage or broken pieces */
                if ((blkHere & COLOR_MASK) == 0x80)
                    return false;
                /* if looking for monosquares, disallow multisquares */
                if (!isMulti && (blkHere & COLOR_MASK) != firstColor)
                    return false;
            }
        }
        return true;
    }

    /**
     * Replaces the 4x4 blocks with a 4x4 square.
     *
     * @param x       column of left side, such that 0 <= x <= playfield width - 4
     * @param y       row of bottom block, such that 0 <= y <= playfield height - 4
     * @param isMulti nonzero for multisquares; 0 for monosquares
     * @return the rows that were changed
     */
    static LJBits markSquare(LJField p, int x, int y, boolean isMulti) {
        int baseBlk = (isMulti ? 0xA0 : 0xB0)
                | CONNECT_MASK;
        for (int i = 0; i < 4; ++i) {
            int c;

            if (i == 0)
                c = baseBlk & ~CONNECT_D;
            else if (i == 3)
                c = baseBlk & ~CONNECT_U;
            else
                c = baseBlk;

            p.b[y + i][x + 0] = c & ~CONNECT_L;
            p.b[y + i][x + 1] = c;
            p.b[y + i][x + 2] = c;
            p.b[y + i][x + 3] = c & ~CONNECT_R;
        }

        return new LJBits(0x0F << y);
    }


    /**
     * Marks all 4x4 squares in the playfield.
     * In the case that a single tetromino forms multiple overlapping
     * squares, prefers gold over silver, high over low, left over right.
     *
     * @param isMulti nonzero for multisquares; 0 for monosquares
     * @return the rows that were changed
     */
    static LJBits findSquares(LJField p, boolean isMulti) {
        LJBits changed = new LJBits();

        for (int y = LJ_PF_HT - 4; y >= 0; --y) {
            for (int x = p.leftWall; x <= p.rightWall - 4; ++x) {
                int baseBlk = p.b[y][x];

                // If this block is filled in and not connected on the left or right
                // then do stuff to it
                if (baseBlk != 0
                        && (baseBlk & (CONNECT_D | CONNECT_L)) == 0
                        && isSquareAt(p, x, y, isMulti)) {
                    changed.add(markSquare(p, x, y, isMulti));
                    p.sounds.add(LJSND.SQUARE);
                }
            }
        }
        return changed;
    }

    static LJBits stickyGluing(LJField p) {
        LJBits changed = new LJBits();
        boolean byColor = (p.gluing.equals(LJGLUING.STICKY_BY_COLOR));

        for (int y = 0; y < LJ_PF_HT; ++y) {
            for (int x = p.leftWall; x < p.rightWall - 1; ++x) {
                int l = p.b[y][x];
                int r = p.b[y][x + 1];
                if (l != 0 && r != 0 &&
                        ((l & CONNECT_R) == 0 || (r & CONNECT_L) == 0) &&
                        (!byColor || ((l ^ r) & COLOR_MASK) == 0)) {
                    p.b[y][x] = l | CONNECT_R;
                    p.b[y][x + 1] = r | CONNECT_L;
                    changed.addBit(y);
                }
            }
        }

        for (int y = 0; y < LJ_PF_HT - 1; ++y) {
            for (int x = p.leftWall; x < p.rightWall; ++x) {
                int b = p.b[y][x];
                int t = p.b[y + 1][x];
                if (b != 0 && t != 0
                        && ((b & CONNECT_U) == 0 || (t & CONNECT_D) == 0)
                        && (!byColor || ((b ^ t) & COLOR_MASK) == 0)) {
                    p.b[y][x] = b | CONNECT_U;
                    p.b[y + 1][x] = t | CONNECT_D;
                    changed.add(3 << y);
                }
            }
        }

        return changed;
    }

    /**
     * Locks the current tetromino in the playfield.
     *
     * @param p the playfield
     * @return the rows in which the tetromino was placed
     */
    static LJBits lockPiece(LJField p) {
        LJBits rows = new LJBits();
        int xBase = p.x;
        int yBase = (int) MathFP.toLong(p.y);
        LJBlkSpec[] blocks = new LJBlkSpec[4];
        for (int i = 0; i < blocks.length; i++) { blocks[i] = new LJBlkSpec(); }

        LJP piece = p.curPiece[0];
        expandPieceToBlocks(blocks, p, piece, xBase, yBase, p.theta);

        p.isSpin = isTspin(p);

        for (int blk = 0; blk < 4; ++blk) {
            int blkY = blocks[blk].y;
            int blkX = blocks[blk].x;
            int blkValue = blocks[blk].conn;

            if (blkValue != 0 && blkY >= 0 && blkY < LJ_PF_HT) {
                rows.addBit(blkY);
                if (blkX >= p.leftWall && blkX < p.rightWall) {
                    p.b[blkY][blkX] = blkValue;
                }
            }
        }
        p.sounds.add(LJSND.LOCK);
        p.alreadyHeld = false;
        p.nLinesThisPiece = 0;

        return rows;
    }

    /**
     * Shuffles the columns of blocks in the playfield.
     *
     * @param p the playfield
     */
    static void shuffleColumns(LJField p) {
        int permu[] = new int[LJ_PF_WID];

        for (int x = p.leftWall; x < p.rightWall; ++x) {
            permu[x] = x;
        }
        for (int x = p.rightWall - 1; x > p.rightWall + 1; --x) {
            int r = ljRandInt(p) % x;
            int t = permu[x];
            permu[x] = permu[r];
            permu[r] = t;
        }

        for (int y = 0; y < LJ_PF_HT; ++y) {
            int blk[] = new int[LJ_PF_WID];

            // Copy blocks to temporary buffer, eliminating left and right connections
            for (int x = p.leftWall; x < p.rightWall; ++x) {
                blk[x] = p.b[y][permu[x]] & ~(CONNECT_L | CONNECT_R);
            }
            for (int x = p.leftWall; x < p.rightWall; ++x) {
                p.b[y][x] = blk[x];
            }
        }
    }

    /**
     * Rotates and shifts a new piece per the rotation system.
     */
    static void rotateNewPiece(LJField p) {
        final LJRotSystem rs = WK.rotSystems.get(p.rotationSystem);
        int shape = p.curPiece[0].index & LJP.MASK.index;
        int kickData = rs.entryOffset[shape];
        p.x += WK.WKX(kickData);
        p.y += MathFP.toFP(WK.WKY(kickData));
        p.theta = rs.entryTheta[shape];

        /* Find extents of piece so that it doesn't enter already collided
   with the walls (otherwise, in Tengen rotation and well width 4,
   spawning I causes block out) */

        LJBlkSpec[] blocks = new LJBlkSpec[4];
        for (int i = 0; i < blocks.length; i++) { blocks[i] = new LJBlkSpec(); }
        int minX = LJ_PF_WID - 1, maxX = 0, maxY = 0;

        expandPieceToBlocks(blocks, p, p.curPiece[0],
                p.x, (int) MathFP.toLong(p.y), p.theta);
        for (int blk = 0; blk < 4; ++blk) {
            if (blocks[blk].conn != 0) {
                if (maxY < blocks[blk].y) {
                    maxY = blocks[blk].y;
                }
                if (maxX < blocks[blk].x) {
                    maxX = blocks[blk].x;
                }
                if (minX > blocks[blk].x) {
                    minX = blocks[blk].x;
                }
            }
        }

        if (minX < p.leftWall) {
            p.x += minX - p.leftWall;
        } else if (maxX >= p.rightWall) {
            p.x -= (maxX + 1) - p.rightWall;
        }
        if (!p.enterAbove && maxY >= p.ceiling) {
            p.y -= MathFP.toFP(maxY - p.ceiling) + 1;
        }
    }


    /**
     * Spawns a tetromino onto the playfield.
     *
     * @param p    the playfield
     * @param hold false for spawning from next; true for swapping with hold
     * @return the rows that were changed by banana effect
     */
    static LJBits newPiece(LJField p, boolean hold) {
        LJBits changed = new LJBits();
        boolean initial = p.state != LJState.FALLING
                && p.state != LJState.LANDED;
        int ihs = (initial && hold) ? 1 : 0;

        if (hold) {
            if (p.state == LJState.LANDED && p.lockReset == LJLOCK.SPAWN) {
                p.speed.lockDelay = p.stateTime;
            }
        } else {
            p.upwardKicks = 0;
        }
        p.x = (LJ_PF_WID - 4) / 2;
        p.dropDist = 0;
        if (ihs == 0) {
            p.state = LJState.FALLING;
            p.stateTime = 0;
        }
        p.isSpin = IsSpin.NONE;
        p.y = MathFP.toFP(p.ceiling - 2);

        /* Note: The gimmick sets the gravity speed after frame() finishes. */

        if (hold) {
            LJP temp;

            if (p.holdStyle != LJHOLD.TO_NEXT) {
                temp = p.holdPiece;
                p.holdPiece = p.curPiece[ihs];
            } else {
                temp = p.curPiece[ihs + 1];
                p.curPiece[ihs + 1] = p.curPiece[ihs];
            }
            p.curPiece[ihs] = temp;
            p.alreadyHeld = true;
            p.sounds.add(LJSND.HOLD);

            // If a negative number was swapped into the current piece,
            // then there was nothing in the hold space (e.g. at the
            // beginning of a round).  In this case, we'll need to fall
            // through and generate a new piece.
            if (temp.index >= 0) {
                rotateNewPiece(p);
                return changed;
            }
        }

        // Shift the next pieces down
        arraycopy(p.curPiece, 1, p.curPiece, 0, LJ_NEXT_PIECES);
        p.sounds.add(LJSND.SPAWN);

        p.curPiece[LJ_NEXT_PIECES] = LJRandom.randomize(p);
        ++p.nPieces;
        if (!p.canRotate) {
            p.theta = (ljRandInt(p) >> 12) & 0x03;
        } else {
            rotateNewPiece(p);
        }

        return changed;
    }

    /**
     * Blanks a playfield and prepares it for a new game.
     * @param p the playfield
     */
    public static void newGame(LJField p) {

        // Clear playfield
        for (int y = 0; y < LJ_PF_HT; y++) {
            for (int x = 0; x < LJ_PF_WID; x++) {
                p.b[y][x] = 0;
            }
        }

        for (int y = 0; y < LJ_MAX_LINES_PER_PIECE; ++y) {
            p.nLineClears[y] = 0;
        }

        if (p.holdStyle == LJHOLD.TNT) {
            LJRandom.initRandomize(p);
            p.holdPiece = LJRandom.randomize(p);
        } else {
            p.holdPiece = LJP.NONE;  // sentinel for no piece in hold box
        }

        // Generate pieces
        LJRandom.initRandomize(p);
        for (int i = 0; i < LJ_NEXT_PIECES; i++) {
            newPiece(p, false);
        }
        p.clearedLines = new LJBits();
        p.nPieces = 0;
        p.state = LJState.NEW_PIECE;
        p.stateTime = 1;
        p.garbage = 0;
        p.outGarbage = 0;
        p.score = 0;
        p.gameTime = 0;
        p.activeTime = 0;
        p.lines = 0;
        p.alreadyHeld = false;
        p.chain = false;
        p.theta = 0;
        p.nLinesThisPiece = 0;
        p.canRotate = true;
        p.speed.entryDelay = 0;
        p.garbageRandomness = 64;
        p.reloaded = 0;

        p.garbageX = ljRandInt(p) % (p.rightWall - p.leftWall) + p.leftWall;
    }

    /**
     * Handles scoring for hard and soft drops.
     *
     * @return false for no change or true for change
     */
    static LJBits scoreDropRows(LJField p, long gravity, long newY) {
        LJBits changed = new LJBits();
        if (gravity > 0) {
            int fallDist = (int) MathFP.toLong(p.y - newY);

            p.dropDist += fallDist;

            // Double scoring for hard drop
            if (p.dropScoreStyle == LJDROP._1S_2H
                    && gravity >= MathFP.toFP(p.ceiling)) {
                fallDist *= 2;
            }
            if (p.dropScoreStyle == LJDROP._1CELL
                    || p.dropScoreStyle == LJDROP._1S_2H) {
                p.score += fallDist;
                changed.dirtyScore = true;
            }

            // Handle scoring for continuous drop
            if (p.dropScoreStyle == LJDROP.NES
                    && newY <= MathFP.toFP(p.hardDropY)) {
                p.score += p.dropDist;
                changed.dirtyScore = true;
                p.dropDist = 0;
            }
        } else {
            p.dropDist = 0;
        }
        return changed;
    }

    /**
     * Handles gravity.
     *
     * @param p         the playfield
     * @param gravity   amount of additional gravity applied by the player
     * @param otherKeys other LJI_* keys being pressed by the player
     *                  (specifically LJI_LOCK)
     */
    static LJBits doPieceGravity(LJField p, long gravity, Set<LJI> otherKeys) {
        LJBits changedRows = new LJBits();

        long newY = p.y - gravity - p.speed.gravity;

        // Check for landed
        if (newY <= MathFP.toFP(p.hardDropY)) {
            newY = MathFP.toFP(p.hardDropY);

            // Downward movement does not result in a T-spin
            if (MathFP.toLong(newY) < MathFP.toLong(p.y)) {
                p.isSpin = IsSpin.NONE;
            }

            changedRows.add(scoreDropRows(p, gravity, newY));
            p.y = newY;

            if (p.state == LJState.FALLING) {
                p.state = LJState.LANDED;
                p.stateTime = p.speed.lockDelay;
                p.sounds.add(LJSND.LAND);
            }
            if (p.stateTime > 0 && !otherKeys.contains(LJI.LOCK)) {
                // lock delay > 128 is a special case for don't lock at all
                if (p.setLockDelay < 128) {
                    --p.stateTime;
                }
            } else {
                LJBits lockRows = lockPiece(p);
                p.state = LJState.LINES;
                p.stateTime = 0;
                changedRows.add(lockRows);
                changedRows.dirtyNext = true;

                // LOCK OUT rule: If a piece locks
                // completely above the ceiling, the game is over.
                if (!((lockRows.bits & ((1 << p.ceiling) - 1)) != 0)) {
                    p.state = LJState.GAMEOVER;
                }
            }
        } else {
            changedRows.add(scoreDropRows(p, gravity, newY));
            p.state = LJState.FALLING;

            // Downward movement does not result in a T-spin
            if (MathFP.toLong(newY) < MathFP.toLong(p.y)) {
                p.isSpin = IsSpin.NONE;
            }
        }
        p.y = newY;
        return changedRows;
    }

    static void updateLockDelayOnMove(LJField p, boolean isUpwardKick) {
        if (p.state == LJState.LANDED) {

            // if tetromino can move down, go back to falling state;
            // otherwise, reset lock delay.
            if (!isCollision(p, p.x, (int) MathFP.toLong(p.y) - 1, p.theta)) {
                p.state = LJState.FALLING;
                if (p.lockReset == LJLOCK.SPAWN) {
                    p.speed.lockDelay = p.stateTime;
                }
                p.stateTime = 0;
            } else {
                p.state = LJState.LANDED;
                if (p.lockReset == LJLOCK.MOVE
                        || (p.lockReset == LJLOCK.STEP && isUpwardKick)) {
                    p.stateTime = p.speed.lockDelay;
                }
            }
        }
    }

    static boolean doRotate(LJField p,
                            int newDir,
                            final int[][] pieceTable,
                            boolean wKicks) {
        int baseX = p.x;
        int baseY = (int) MathFP.toLong(p.y);

        int withKicks = wKicks ? KICK_TABLE_LEN : 1;

        // allow specifying null tables for O
        if (pieceTable == null) {
            if (!isCollision(p, baseX, baseY, newDir)) {
                p.theta = newDir;
                p.sounds.add(LJSND.ROTATE);
                return true;
            }
            return false;
        }

        final int[] table = pieceTable[newDir];
        int baseKickY = -1000;  // sentinel for uninitialized

        for (int kick = 0; kick < withKicks; kick++) {
            int kickData = table[kick];
            if (kickData == WK.WK_END) {
                break;
            } else if (kickData == WK.ARIKA_IF_NOT_CENTER) {

                // Compute the free space position
                kickData = table[0];
                int kickX = WK.WKX(kickData) + baseX;
                int kickY = WK.WKY(kickData) + baseY;
                LJBlkSpec[] blocks = new LJBlkSpec[4];
                for (int i = 0; i < blocks.length; i++) { blocks[i] = new LJBlkSpec(); }
                boolean allowed = false;

                // If a block other than the center column of this position
                // is occupied, go to the next step (that is,
                // allow subsequent kicks)
                expandPieceToBlocks(blocks, p, p.curPiece[0],
                        kickX, kickY, newDir);

                for (int blk = 0; blk < 4; ++blk) {
                    if (blocks[blk].conn != 0
                            && blocks[blk].x != baseX + 1
                            && isOccupied(p, blocks[blk].x, blocks[blk].y)) {
                        allowed = true;
                        break;
                    }
                }

                // Otherwise, only blocks of the center column are occupied,
                // and these cannot kick the piece.
                if (!allowed) {
                    return false;
                }
            } else {
                int kickX = WK.WKX(kickData) + baseX;
                int kickY = WK.WKY(kickData) + baseY;

                // If this is the first
                if (baseKickY == -1000) {
                    baseKickY = kickY;
                }
                if ((kickY <= baseKickY || p.upwardKicks < p.maxUpwardKicks)
                        && !isCollision(p, kickX, kickY, newDir)) {
                    p.theta = newDir;
                    p.x = kickX;
                    if (kickY > baseKickY) {
                        p.y = MathFP.toFP(kickY);

                        // on the FIRST floor kick of a piece, reset lock delay
                        if (p.upwardKicks == 0) {
                            updateLockDelayOnMove(p, true);
                        }
                        ++p.upwardKicks;
                    } else if (kickY < baseKickY) {
                        p.y = MathFP.toFP(kickY) + 0xFFFF;
                    } else {
                        p.y = MathFP.toFP(kickY) + (p.y & 0xFFFF);
                    }
                    p.sounds.add(LJSND.ROTATE);
                    return true;
                }
            }
        }
        return false;
    }


    /**
     * Tries to rotate the current piece 90 degrees counterclockwise,
     * using the counterclockwise wall kick tables.
     *
     * @param p         the playfield
     * @param withKicks nonzero for wall kick, zero for none
     */
    static boolean doRotateLeft(LJField p, boolean withKicks) {
        int newDir = (p.theta + 3) & 0x03;
        final LJRotSystem rs = WK.rotSystems.get(p.rotationSystem);
        int tableNo = rs.kicksL[p.curPiece[0].index & LJP.MASK.index];
        final int[][] pieceTable = tableNo >= 0
                ? (rs.kickTables[tableNo])
                : null;
        return doRotate(p, newDir, pieceTable, withKicks);
    }

    /**
     * Tries to rotate the current piece 90 degrees clockwise,
     * using the clockwise wall kick tables.
     *
     * @param p         the playfield
     * @param withKicks nonzero for wall kick, zero for none
     */
    static boolean doRotateRight(LJField p, boolean withKicks) {
        int newDir = (p.theta + 1) & 0x03;
        final LJRotSystem rs = WK.rotSystems.get(p.rotationSystem);
        int tableNo = rs.kicksR[p.curPiece[0].index & LJP.MASK.index];
        final int[][] pieceTable = tableNo >= 0
                ? (rs.kickTables[tableNo])
                : null;
        return doRotate(p, newDir, pieceTable, withKicks);
    }

    static LJBits checkLines(final LJField p, LJBits checkRows) {
        LJBits foundLines = new LJBits();
        for (int y = 0;
             y < LJ_PF_HT && checkRows.bits != 0;
             ++y, checkRows.bits >>= 1) {
            if ((checkRows.bits & 1) != 0) {
                final int[] row = p.b[y];
                boolean found = true;

                for (int x = p.leftWall; x < p.rightWall && found; ++x) {
                    found = found && (row[x] != 0);
                }
                if (found) {
                    foundLines.addBit(y);
                }
            }
        }

        return foundLines;
    }

    static void fillCLoop(LJField p, int x, int y, int src, int dst) {
        int fillL, fillR, i;

        fillL = fillR = x;
        do {
            p.c[y][fillL] = dst;
            fillL--;
        } while ((fillL >= p.leftWall) && (p.c[y][fillL] == src));
        fillL++;

        do {
            p.c[y][fillR] = dst;
            fillR++;
        } while ((fillR < p.rightWall) && (p.c[y][fillR] == src));
        fillR--;

        for (i = fillL; i <= fillR; i++) {
            if (y > 0 && p.c[y - 1][i] == src) {
                fillCLoop(p, i, y - 1, src, dst);
            }
            if (y < LJ_PF_HT - 1 && p.c[y + 1][i] == src) {
                fillCLoop(p, i, y + 1, src, dst);
            }
        }
    }


    static void fillC(LJField p, int x, int y, int dstC) {
        if (p.c[y][x] != dstC) {
            fillCLoop(p, x, y, p.c[y][x], dstC);
        }
    }

    /**
     * Locks the block regions that have landed.
     *
     * @param p the playfield
     */
    static void lockLandedRegions(LJField p) {
        // Look for regions that are on top of ground regions, where
        // "ground regions" are any block that is solid and whose region ID is 0.
        for (int landed = 1; landed != 0;) {
            landed = 0;
            // If something hit the ground, erase its floating bit
            for (int y = 0; y < LJ_PF_HT; ++y) {
                for (int x = p.leftWall; x < p.rightWall; ++x) {
                    // If there's a floating block here, and a not-floating block below,
                    // erase this block group's floatiness
                    if (p.c[y][x] != 0 &&
                            (y == 0 || (!(p.c[y - 1][x] != 0) && (p.b[y - 1][x] != 0)))) {
                        fillC(p, x, y, 0);
                        p.sounds.add(LJSND.LAND);
                        landed = 1;
                    }
                }
            }
        }
    }

    /**
     * Separates the playfield into regions that shall fall separately.
     *
     * @param p        the playfield
     * @param byColors false: Touching blocks form a region.
     *                 true: Touching blocks of a single color form a region.
     */
    static void stickyMark(LJField p, boolean byColors) {
        for (int y = 0; y < LJ_PF_HT; ++y) {
            for (int x = p.leftWall; x < p.rightWall; ++x) {
                int blkHere = p.b[y][x] & COLOR_MASK;

                if (!byColors) {
                    blkHere = blkHere != 0 ? 0x10 : 0;
                }
                p.c[y][x] = blkHere;
            }
        }

        if (byColors) {
            lockLandedRegions(p);
        } else {
            // mark the bottom row as landed
            for (int x = p.leftWall; x < p.rightWall; ++x) {
                if (p.c[0][x] != 0) {
                    fillC(p, x, 0, 0);
                }
            }
        }

        //p.stateTime = 5;
    }


    /**
     * Sets the color of a piece to gray/garbage (0x80).
     *
     * @param x   column of a block in the piece
     * @param y   row of a block in the piece
     * @param rgn the region ID
     */
    static void cascadeMarkPiece(LJField p, int x, int y, int rgn) {
        int blkHere = p.b[y][x];

        if (blkHere != 0 && p.c[y][x] == 0) {
            p.c[y][x] = rgn;
            if ((blkHere & CONNECT_D) != 0 && y > 0)
                cascadeMarkPiece(p, x, y - 1, rgn);
            if ((blkHere & CONNECT_U) != 0 && y < LJ_PF_HT - 1)
                cascadeMarkPiece(p, x, y + 1, rgn);
            if ((blkHere & CONNECT_L) != 0 && x > p.leftWall)
                cascadeMarkPiece(p, x - 1, y, rgn);
            if ((blkHere & CONNECT_R) != 0 && x < p.rightWall - 1)
                cascadeMarkPiece(p, x + 1, y, rgn);
        }
    }

    static void cascadeMark(LJField p) {
        int rgn = 0;

        for (int y = 0; y < LJ_PF_HT; ++y) {
            for (int x = p.leftWall; x < p.rightWall; ++x) {
                p.c[y][x] = 0;
            }
        }
        for (int y = 0; y < LJ_PF_HT; ++y) {
            for (int x = p.leftWall; x < p.rightWall; ++x) {
                cascadeMarkPiece(p, x, y, ++rgn);
            }
        }
        lockLandedRegions(p);
        //p.stateTime = 5;
    }

    static void breakEverything(LJField p) {
        for (int y = 0; y < LJ_PF_HT; ++y) {
            for (int x = p.leftWall; x < p.rightWall; ++x) {
                if (p.b[y][x] != 0) {
                    p.c[y][x] = x + 1;
                    p.b[y][x] = 0x80;
                } else {
                    p.c[y][x] = 0;
                }
            }
        }

        // fill bottom row
        for (int x = p.leftWall; x < p.rightWall; ++x) {
            if (p.c[0][x] != 0) {
                fillC(p, x, 0, 0);
            }
        }
        p.stateTime = 5;
    }

    /**
     * Sets the color of a piece to gray/garbage (0x80).
     *
     * @param x column of a block in the piece
     * @param y row of a block in the piece
     */
    static LJBits breakPiece(LJField p, int x, int y) {
        LJBits changed = new LJBits();
        int blkHere = p.b[y][x];
        int colorHere = blkHere & COLOR_MASK;
        int connHere = blkHere & CONNECT_MASK;

        if (colorHere != 0x80) {
            p.b[y][x] = connHere | 0x80;
            changed.addBit(y);
            if ((blkHere & CONNECT_D) != 0 && y > 0)
                changed.add(breakPiece(p, x, y - 1));
            if ((blkHere & CONNECT_U) != 0 && y < LJ_PF_HT - 1)
                changed.add(breakPiece(p, x, y + 1));
            if ((blkHere & CONNECT_L) != 0 && x > p.leftWall)
                changed.add(breakPiece(p, x - 1, y));
            if ((blkHere & CONNECT_R) != 0 && x < p.rightWall - 1)
                changed.add(breakPiece(p, x + 1, y));
        }
        return changed;
    }

    /**
     * Removes blocks in cleared lines from the playfield and marks
     * remaining blocks for gravity.
     *
     * @param foundLines the lines to be cleared
     * @return the rows that were changed
     */
    static LJBits clearLines(LJField p, LJBits foundLines) {
        LJBits changed = foundLines.clone();

        p.clearedLines = foundLines.clone();
        if (foundLines.bits != 0) {
            p.sounds.add(LJSND.LINE);
        }
        // Iterate through all bits in the bitfield
        for (int y = 0;
             y < LJ_PF_HT && foundLines.bits != 0;
             ++y, foundLines.bits >>= 1) {
            if ((foundLines.bits & 1) != 0) {

                // In square mode, turn broken pieces (but not 4x4 squares)
                // into garbage blocks
                if (p.gluing == LJGLUING.SQUARE) {
                    for (int x = p.leftWall; x < p.rightWall; ++x) {
                        if (p.b[y][x] < 0x80) {
                            changed.add(breakPiece(p, x, y));
                        } else if ((p.b[y][x] & (0xF0 | CONNECT_R)) == 0xA0) {
                            p.score += 500;
                            changed.dirtyScore = true;
                        } else if ((p.b[y][x] & (0xF0 | CONNECT_R)) == 0xB0) {
                            p.score += 1000;
                            changed.dirtyScore = true;
                        }
                    }
                }

                for (int x = p.leftWall; x < p.rightWall; ++x) {
                    p.b[y][x] = 0;
                }

                // break connections up and down (like Tengen Tetyais)
                if (y > 0) {
                    for (int x = p.leftWall; x < p.rightWall; ++x) {
                        p.b[y - 1][x] &= ~CONNECT_U;
                    }
                    changed.addBit(y - 1);
                }
                if (y < LJ_PF_HT - 1) {
                    for (int x = p.leftWall; x < p.rightWall; ++x) {
                        p.b[y + 1][x] &= ~CONNECT_D;
                    }
                    changed.addBit(y + 1);
                }
            }
        }
        if (p.gluing == LJGLUING.SQUARE && p.isSpin != IsSpin.NONE) {
            breakEverything(p);
            changed.add((1 << LJ_PF_HT) - 1);
        } else if (p.clearGravity == LJGRAV.STICKY) {
            stickyMark(p, false);
        } else if (p.clearGravity == LJGRAV.STICKY_BY_COLOR) {
            stickyMark(p, true);
        } else if (p.clearGravity == LJGRAV.CASCADE) {
            cascadeMark(p);
        } else {
            p.stateTime = 0;
        }

        return changed;
    }

    static int stickyFallLines(LJField p) {
        int minY = LJ_PF_HT;

        // Move floating stuff down by one block
        for (int y = 1; y < LJ_PF_HT; ++y) {
            for (int x = p.leftWall; x < p.rightWall; ++x) {
                int c = p.c[y][x];
                if (c != 0) {
                    p.c[y - 1][x] = c;
                    p.c[y][x] = 0;
                    p.b[y - 1][x] = p.b[y][x];
                    p.b[y][x] = 0;

                    if (minY > y) {
                        minY = y;
                    }
                }
            }
        }

        // If we're done, skip all the rest
        if (minY >= LJ_PF_HT) {
            return LJ_PF_HT;
        }

        lockLandedRegions(p);
        return minY - 1;
    }


    /**
     * Counts the number of trailing zero bits on an integer.
     * For instance, 00000000 00000000 00000001 11111000
     * has 3 trailing zeroes.
     */
    public static int bfffo(LJBits row) {
        LJBits rowBits = row.clone();
        int lineRow = 0;

        if (rowBits.bits == 0) {
            return 32;
        }
        if ((rowBits.bits & 0xFFFF) == 0) {
            rowBits.bits >>= 16;
            lineRow += 16;
        }
        if ((rowBits.bits & 0xFF) == 0) {
            rowBits.bits >>= 8;
            lineRow += 8;
        }
        if ((rowBits.bits & 0xF) == 0) {
            rowBits.bits >>= 4;
            lineRow += 4;
        }
        if ((rowBits.bits & 0x3) == 0) {
            rowBits.bits >>= 2;
            lineRow += 2;
        }
        if ((rowBits.bits & 0x1) == 0) {
            rowBits.bits >>= 1;
            lineRow += 1;
        }
        return lineRow;
    }

    static int fallLines(LJField p) {
        LJBits rowBits = p.tempRows.clone();
        int lineRow = 0;

        if (p.clearGravity != LJGRAV.NAIVE
                || (p.gluing == LJGLUING.SQUARE && p.isSpin != IsSpin.NONE)) {
            return stickyFallLines(p);
        }

        if (rowBits.bits == 0) {
            return LJ_PF_HT;
        }

        lineRow = bfffo(rowBits);
        p.tempRows.bits = (p.tempRows.bits & (-2 << lineRow)) >> 1;
        if ((p.tempRows.bits & (1 << lineRow)) == 0) {
            p.sounds.add(LJSND.LAND);
        }

        // Move stuff down by 1 row
        for (int y = lineRow; y < LJ_PF_HT - 1; ++y) {
            int[] row0 = p.b[y];
            int[] row1 = p.b[y + 1];
            arraycopy(row1, p.leftWall, row0, p.leftWall, p.rightWall - p.leftWall);
        }

        // Clear top row
        for (int x = p.leftWall; x < p.rightWall; ++x) {
            p.b[LJ_PF_HT - 1][x] = 0;
        }

        return lineRow;
    }

    /**
     * Counts the number of 1 bits in a bitfield.
     *
     * @param bRef the bitfield
     * @return the number of bits in p with a value of 1.
     */
    static int countOnes(LJBits bRef) {
        int ones = 0;
        LJBits b = bRef.clone();
        while (b.bits != 0) {
            ++ones;
            b.bits &= b.bits - 1;
        }
        return ones;
    }

    static int addGarbage(LJField p) {
        // Move stuff up by 1 row
        for (int y = LJ_PF_HT - 2; y >= 0; --y) {
            int[] row1 = p.b[y + 1];
            int[] row0 = p.b[y];
            arraycopy(row0, p.leftWall, row1, p.leftWall, p.rightWall - p.leftWall);
        }

        // Garbage in bottom row
        for (int x = p.leftWall; x < p.rightWall; ++x) {
            p.b[0][x] = 0x80;
        }

        // Randomize location of garbage hole
        int r = ljRandInt(p) >> 7 & 0xFF;
        int garbageX = r <= p.garbageRandomness ? ljRandInt(p) % (p.rightWall - p.leftWall) + p.leftWall : p.garbageX;
        p.b[0][garbageX] = 0;
        p.garbageX = garbageX;

        // Horizontally connect the blocks that make up garbage in bottom row
        for (int x = p.leftWall; x < p.rightWall - 1; ++x) {
            if (p.b[0][x] != 0 && p.b[0][x + 1] != 0) {
                p.b[0][x] |= CONNECT_R;
                p.b[0][x + 1] |= CONNECT_L;
            }
        }

        // Vertically connect the blocks that make up garbage in bottom row
        for (int x = p.leftWall; x < p.rightWall; ++x) {
            if (p.b[0][x] != 0
                    && ((p.b[1][x] & COLOR_MASK) == 0x80)) {
                p.b[0][x] |= CONNECT_U;
                p.b[1][x] |= CONNECT_D;
            }
        }

        return (1 << LJ_PF_VIS_HT) - 1;
    }

    /**
     * Things to do just before launching a new piece.
     */
    static void prepareForNewPiece(LJField p) {
        int nLines = p.nLinesThisPiece;

        // Add to number of clears of each number of lines.
        int idx;

        if (p.clearGravity == LJGRAV.NAIVE) {
            // In naive gravity, T-spin single, double, and triple counts
            // are stored in 5-row, 6-row, and 7-row slots, and T-spins
            // that clear 0 lines are not counted.
            idx = (nLines > 4)
                    ? 4
                    : nLines;

            if (nLines >= 1 && p.isSpin != IsSpin.NONE) {
                idx += 4;
            }
        } else {
            idx = (nLines > LJ_MAX_LINES_PER_PIECE)
                    ? LJ_MAX_LINES_PER_PIECE
                    : nLines;
        }

        if (nLines < 4 && p.isSpin == IsSpin.NONE && p.garbageStyle == LJGARBAGE.HRDERBY) {
            p.garbage += nLines;
        }

        ljAssert(p,
                idx <= LJ_MAX_LINES_PER_PIECE,
                "Number of lines cleared with last piece out of bounds in prepareForNewPiece");
        if (idx > 0) {
            p.nLineClears[idx - 1] += 1;
        }

        p.state = LJState.NEW_PIECE;
        p.stateTime = p.speed.entryDelay;
    }

    /**
     * Runs one frame of S.M.G.
     *
     * @param p  the playfield
     * @param in the player's input
     * @return the rows that were modified
     */
    public static LJBits frame(LJField p, final LJInput in) {
        LJBits changedRows = new LJBits();
        LJBits tempRows;
        int distance;
        boolean moved = false;
        boolean isFirstFrame = p.sounds.contains(LJSND.SPAWN) &&
                p.sounds.contains(LJSND.HOLD);

        p.sounds = EnumSet.noneOf(LJSND.class);

        // Make hold work at ANY time.
        if ((in.other.contains(LJI.HOLD))
                && p.holdStyle != LJHOLD.NONE
                && !p.alreadyHeld) {
            changedRows.add(newPiece(p, true));
            changedRows.dirtyNext = true;
            updHardDropY(p);
        }

        switch (p.state) {
            case NEW_PIECE:
                if (p.garbage > 0) {
                    changedRows.add(addGarbage(p));
                    --p.garbage;
                    break;
                }

                // ARE
                if (p.stateTime > 0) {
                    --p.stateTime;
                }
                if (p.stateTime > 0) {
                    break;
                }

                changedRows.add(newPiece(p, false));
                updHardDropY(p);
                changedRows.dirtyNext = true;

                /* If the piece spawns over blocks, this is a "block out" and a
        loss under most rules.  But skip checking it now so that
        the player can IRS out of block out. */

                break;

                // the following executes for both falling and landed
            case FALLING:
            case LANDED:
                ++p.activeTime;
                if (p.canRotate) {
                    int oldX = p.x;
                    int oldY = (int) MathFP.toLong(p.y);
                    distance = in.rotation;
                    for (; distance < 0; ++distance) {
                        // 0.43: Do not apply wall kicks on the first frame (IRS)
                        if (doRotateLeft(p, !isFirstFrame)) {
                            moved = true;

                            // isSpin == 1: twist in place
                            // isSpin == 2: twist with kick
                            // if (p.tSpinAlgo == LJTS_TDS_NO_KICK)
                            // then only isSpin == 1 is worth points.
                            if (p.x == oldX && MathFP.toLong(p.y) == oldY) {
                                p.isSpin = IsSpin.TWIST;
                            } else {
                                p.isSpin = IsSpin.TWISTKICK;
                            }
                        } else {
                            break;
                        }
                    }
                    for (; distance > 0; --distance) {
                        if (doRotateRight(p, !isFirstFrame)) {
                            moved = true;
                            if (p.x == oldX && MathFP.toLong(p.y) == oldY) {
                                p.isSpin = IsSpin.TWIST;
                            } else {
                                p.isSpin = IsSpin.TWISTKICK;
                            }
                        } else {
                            break;
                        }
                    }
                }

                /* If the piece spawns over blocks, this is a "block out" and a
        loss under most rules.  Check it now, after rotation, so that
        the player can IRS out of block out. */
                if (isFirstFrame
                        && isCollision(p, p.x, (int) MathFP.toLong(p.y), p.theta)) {
                    changedRows.add(lockPiece(p));
                    p.state = LJState.GAMEOVER;
                    System.out.println("game over!");
                }

                distance = in.movement;
                for (; distance < 0; ++distance) {
                    if (!isCollision(p, p.x - 1, (int) MathFP.toLong(p.y), p.theta)) {
                        --p.x;
                        p.sounds.add(LJSND.SHIFT);
                        moved = true;
                        p.isSpin = IsSpin.NONE;
                    }
                }
                for (; distance > 0; --distance) {
                    if (!isCollision(p, p.x + 1, (int) MathFP.toLong(p.y), p.theta)) {
                        ++p.x;
                        p.sounds.add(LJSND.SHIFT);
                        moved = true;
                        p.isSpin = IsSpin.NONE;
                    }
                }
                updHardDropY(p);
                if (p.state != LJState.GAMEOVER) {
                    if (moved) {
                        updateLockDelayOnMove(p, false);
                    }
                    tempRows = doPieceGravity(p, MathFP.toFP(in.gravity) >> 3, in.other);
                    p.tempRows = tempRows;
                    changedRows.add(tempRows);
                }

                // At this point, if the piece locked,
                // p.tempRows holds the rows in which the piece landed.
                break;

            case LINES:
                if (p.stateTime > 0) {
                    --p.stateTime;
                }
                if (p.stateTime > 0) {
                    break;
                }
                if (p.gluing == LJGLUING.SQUARE) {
                    LJBits gluedRows = findSquares(p, false);
                    gluedRows.add(findSquares(p, true));
                    changedRows.add(gluedRows);
                    if (gluedRows.bits != 0) {

                        // When a 4x4 block square is formed, a delay
                        // equal to the line delay is added.
                        p.stateTime += p.speed.lineDelay;
                        break;
                    }
                } else if (p.gluing == LJGLUING.STICKY
                        || p.gluing == LJGLUING.STICKY_BY_COLOR) {
                    changedRows.add(stickyGluing(p));
                }

                // At this point, p.tempRows holds the rows in which
                // a line could possibly have been made.
                tempRows = p.tempRows;
                tempRows = checkLines(p, tempRows);
                p.tempRows = tempRows;
                // At this point, p.tempRows holds the rows in which
                // a line HAS been made.
                Gimmicks.addLinesScore(p, tempRows);
                changedRows.dirtyScore = true;

                // At this point, p.tempRows holds the rows in which a line
                // HAS been made.
                p.clearedLines = tempRows;
                if (tempRows.bits == 0) {
                    prepareForNewPiece(p);
                    break;
                }

                changedRows.add(clearLines(p, tempRows.clone()));

                p.state = LJState.LINES_FALLING;
                p.stateTime += p.speed.lineDelay;
                break;

            case LINES_FALLING:
                if (p.stateTime > 0) {
                    --p.stateTime;
                }
                if (p.stateTime > 0) {
                    break;
                }
                int moved2 = fallLines(p);
                if (moved2 >= LJ_PF_HT) {
                    p.state = LJState.LINES;
                    p.tempRows.bits = (1 << LJ_PF_HT) - 1;
                }
                changedRows.add((~0 << moved2) & ((1 << LJ_PF_VIS_HT) - 1));
                break;

            default:
    break;

  }

  ++p.gameTime;
  return changedRows;
}

}
